package com.flextoolbox.controls
{
	import com.flextoolbox.events.PopUpThumbnailEvent;
	import com.flextoolbox.utils.TheInstantiator;
	
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.Loader;
	import flash.display.PixelSnapping;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.utils.setTimeout;
	
	import mx.controls.Image;
	import mx.controls.ProgressBar;
	import mx.core.BitmapAsset;
	import mx.core.IFlexDisplayObject;
	import mx.core.mx_internal;
	import mx.effects.Effect;
	import mx.effects.Move;
	import mx.effects.Parallel;
	import mx.effects.Resize;
	import mx.events.CloseEvent;
	import mx.events.EffectEvent;
	import mx.managers.PopUpManager;
	import mx.styles.CSSStyleDeclaration;
	import mx.styles.StyleManager;
	
	use namespace mx_internal;
	
	//----------------------------------
	//  Styles
	//----------------------------------
	
	/**
	 * The minimum number of pixels between the popup image and the edge of the
	 * application.
	 * 
	 * @default 100
	 */
	[Style(name="popUpBorderSize", type="Number")]
	
	/**
	 * The number of milliseconds for which the animation is played when the
	 * popup is shown or removed.
	 * 
	 * @default 150
	 */
	[Style(name="popUpDuration", type="int")]
	
	/**
	 * If the pop-up content must be loaded from a URL, a progress indicator is
	 * displayed for the user. Normally, this is just a basic animation, but
	 * there is one special case. If the <code>progressIndicator</code> style is
	 * set to display a ProgressBar control, it will automatically get updated
	 * with the actual progress.
	 * 
	 * @default mx.controls.ProgressBar
	 */
	[Style(name="progressIndicator", type="Class")]
	
	/**
	 * If the progressIndicator is a style client, this style name will be
	 * applied.
	 * 
	 * @default null
	 */
	[Style(name="progressIndicatorStyleName", type="String")]
	
	//----------------------------------
	//  Events
	//----------------------------------
	
	/**
	 * Dispatched when the user clicks a thumbnail to display the pop-up
	 * content.
	 */
	[Event(name="popUpOpen", type="com.flextoolbox.events.PopUpThumbnailEvent")]
	
	/**
	 * Dispatched when the user clicks anywhere on the screen while pop-up
	 * content is displayed to close the pop-up content.
	 */
	[Event(name="popUpClose", type="com.flextoolbox.events.PopUpThumbnailEvent")]
	
	/**
	 * A thumbnail representation of an image that, when clicked, will display
	 * the full-size image as a modal popup. 
	 * 
	 * @author Josh Tynjala
	 */
	public class PopUpThumbnail extends Image
	{
		
	//----------------------------------
	//  Static Properties
	//----------------------------------
		
		/**
		 * @private
		 * If no thumbnailSource is specified, and neither width nor height are
		 * explicitly defined, this value is used rather than display the
		 * generated thumbnail at full size.
		 */
		private static const DEFAULT_GENERATED_THUMBNAIL_SIZE:Number = 120;
		
	//----------------------------------
	//  Static Methods
	//----------------------------------
		
		/**
		 * @private
		 * Sets up the default style values for controls of this type.
		 */
		private static function initializeStyles():void
		{
			var styles:CSSStyleDeclaration = StyleManager.getStyleDeclaration("PopUpThumbnail");
			if(!styles)
			{	
				styles = new CSSStyleDeclaration();
			}
			
			styles.defaultFactory = function():void
			{
				this.popUpBorderSize = 100;
				this.popUpDuration = 150;
				this.progressIndicator = ProgressBar;
			}
			StyleManager.setStyleDeclaration("PopUpThumbnail", styles, false);
		}
		initializeStyles();
		
	//----------------------------------
	//  Constructor
	//----------------------------------
	
		/**
		 * Constructor.
		 */
		public function PopUpThumbnail()
		{
			super();
			this.buttonMode = true;
			this.addEventListener(MouseEvent.CLICK, imageClickHandler);
		}
		
	//----------------------------------
	//  Properties
	//----------------------------------
	
		/**
		 * @private
		 * This is the initial width value of the content. It is used to
		 * maintain the aspect ratio when measuring.
		 */
		protected var popUpContentWidth:Number;
		
		/**
		 * @private
		 * This is the initial height value of the content. It is used to
		 * maintain the aspect ratio when measuring.
		 */
		protected var popUpContentHeight:Number;
		
		/**
		 * @private
		 * Another image control that is displayed when the user clicks on this
		 * control.
		 */
		protected var popUpImage:Image;
		
		/**
		 * @private
		 * An animation or another indicator the the content is loading.
		 */
		protected var loadingIndicator:IFlexDisplayObject;
		
		/**
		 * @private
		 * The effect that is played when the popup is shown or removed.
		 */
		protected var popUpEffect:Effect;
		
		/**
		 * @private
		 * Flag that indicates that the popup content hasn't been fully
		 * initialized yet.
		 */
		protected var showPopUpAfterLoading:Boolean = false;
		
		/**
		 * @private
		 * Flag indicating that the source property has changed.
		 */
		protected var sourceChanged:Boolean = false;
		
		/**
		 * @private
		 * Storage for the source property.
		 */
		private var _source:Object;
		
		/**
		 * The URL, object, class or string name of a class to load as the
		 * pop-up content. If the related <code>thumbnailSource</code> property
		 * is set to <code>null</code>, this source is also used to generate a
		 * bitmap snapshot to display as the thumbnail.
		 * 
		 * The value of the source property represents a relative or
		 * absolute URL; a ByteArray representing a SWF, GIF, JPEG, or PNG; an
		 * object that implements IFlexDisplayObject; a class whose type
		 * implements IFlexDisplayObject; or a String that represents a class.
		 * 
		 * @see #thumbnailSource
		 */
		override public function get source():Object
		{
			return this._source;
		}
		
		/**
		 * @private
		 */
		override public function set source(value:Object):void
		{
			this._source = value;
			this.sourceChanged = true;
			this.invalidateProperties();
			this.invalidateSize();
			this.invalidateDisplayList();
		}
		
		/**
		 * @private
		 * Flag indicating that the thumbnailSource property has changed.
		 */
		protected var thumbnailSourceChanged:Boolean = false;
		
		/**
		 * @private
		 * Storage for the thumbnailSource property.
		 */
		private var _thumbnailSource:Object;
		
		/**
		 * The URL, object, class or string name of a class to load as the
		 * thumbnail content. If the <code>thumbnailSource</code> property is
		 * <code>null</code>, the thumbnail will be automatically generated
		 * as a bitmap snapshot of the <code>source</code>.
		 * 
		 * The value of the thumbnailSource property represents a relative or
		 * absolute URL; a ByteArray representing a SWF, GIF, JPEG, or PNG; an
		 * object that implements IFlexDisplayObject; a class whose type
		 * implements IFlexDisplayObject; or a String that represents a class.
		 * 
		 * @see #source
		 * @default null
		 */
		public function get thumbnailSource():Object
		{
			return this._thumbnailSource;
		}
		
		/**
		 * @private
		 */
		public function set thumbnailSource(value:Object):void
		{
			this._thumbnailSource = value;
			this.thumbnailSourceChanged = true;
			this.invalidateProperties();
			this.invalidateSize();
			this.invalidateDisplayList();
		}
		
		/**
		 * @private
		 * Storage for the clickToClosePopUp property.
		 */
		private var _clickToClosePopUp:Boolean = true;

		/**
		 * If <code>true</code>, and the pop-up content is displayed, the user
		 * may click anywhere on the screen to close the pop-up content.
		 * Advanced developers may set this to <code>false</code> to close the
		 * content using a different method. The content must dispatch a
		 * <code>CloseEvent.CLOSE</code> event when it is ready to be closed. 
		 */
		public function get clickToClosePopUp():Boolean
		{
			return this._clickToClosePopUp;
		}

		/**
		 * @private
		 */
		public function set clickToClosePopUp(value:Boolean):void
		{
			this._clickToClosePopUp = value;
		}

	//----------------------------------
	//  Protected Methods
	//----------------------------------
	
		/**
		 * @private
		 */
		override protected function commitProperties():void
		{
			if(this.thumbnailSourceChanged)
			{
				super.source = this._thumbnailSource;
				this.thumbnailSourceChanged = false;
			}
			
			if(this.sourceChanged)
			{
				//if we don't have a thumbnail defined, then it has to be
				//created, so the popup has to be loaded prematurely.
				if(!this._thumbnailSource)
				{
					this.loadPopUpContent();
				}
				this.sourceChanged = false;
			}
			
			super.commitProperties();
		}
		
		/**
		 * @private
		 * Initialize the pop-up image.
		 */
		protected function loadPopUpContent(showAfterLoading:Boolean = false):void
		{
			if(this.popUpImage)
			{
				//get rid of the previous popup content if it exists
				//when I try to reset the source property, I get a runtime error
				//somewhere in mx.controls.SWFLoader. The workaround is to
				//recreate the image control every time...
				this.popUpImage.unloadAndStop();
				this.popUpImage = null;
			}
			
			this.popUpImage = new Image();
			this.popUpImage.setStyle("horizontalAlign", "center");
			this.popUpImage.setStyle("verticalAlign", "middle");
			this.popUpImage.source = this._source;
			this.popUpImage.validateProperties();
			
			if(!this._source)
			{
				this.popUpContentWidth = 0;
				this.popUpContentHeight = 0;
				super.source = null;
				return;
			}
			
			//add it to the pop-up manager immediately, even if it isn't loaded
			//we don't want the user interacting while it is loading.
			if(showAfterLoading)
			{
				this.displayPopUp();
				this.displayLoadingIndicator();
			}
			
			//if we don't have to download the content, we can get the width and
			//height right away. otherwise, we have to wait.
			if(this.popUpImage.contentHolder is Loader)
			{
				var contentLoader:Loader = Loader(this.popUpImage.contentHolder);
				
				//if the content is already loaded (surprising, but it
				//happens. probably because it gets cached), then grab the
				//content's width and height
				if(this.popUpImage.content && contentLoader.contentLoaderInfo.bytesLoaded == contentLoader.contentLoaderInfo.bytesTotal)
				{
					this.popUpContentWidth = this.popUpImage.contentWidth;
					this.popUpContentHeight = this.popUpImage.contentHeight;
				}
				else
				{
					//wait to load
					this.showPopUpAfterLoading = showAfterLoading;
					contentLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, popUpContentLoadCompleteHandler);
					return;
				}
			}
			else
			{
				this.popUpContentWidth = this.popUpImage.contentWidth;
				this.popUpContentHeight = this.popUpImage.contentHeight;
			}
			
			this.generateThumbnail();
			if(showAfterLoading)
			{
				this.animatePopUp(true);
			}
		}
		
		/**
		 * @private
		 * Adds the popup to the popupmanager, initializes some things, and
		 * dispatches the open event.
		 */
		protected function displayPopUp():void
		{
			PopUpManager.addPopUp(this.popUpImage, this, true);
			
			this.popUpImage.mouseEnabled = !this._clickToClosePopUp;
			this.popUpImage.mouseChildren = !this._clickToClosePopUp;
			
			var popUpOpen:PopUpThumbnailEvent = new PopUpThumbnailEvent(PopUpThumbnailEvent.POP_UP_OPEN);
			this.dispatchEvent(popUpOpen);
		}
		
		/**
		 * @private
		 * Adds the loading indicator to the pop up manager. To be used when the
		 * content must be loaded from an external source.
		 */
		protected function displayLoadingIndicator():void
		{
			var progressIndicator:Object = this.getStyle("progressIndicator");
			var progressIndicatorStyleName:String = this.getStyle("progressIndicatorStyleName");
			this.loadingIndicator = TheInstantiator.newInstance(progressIndicator, progressIndicatorStyleName) as IFlexDisplayObject;
			if(!this.loadingIndicator)
			{
				return;	
			}
			
			if(this.loadingIndicator is ProgressBar)
			{
				var progress:ProgressBar = ProgressBar(this.loadingIndicator);
				progress.source = this.popUpImage;
			}
			PopUpManager.addPopUp(this.loadingIndicator, DisplayObject(this.parentApplication));
			
			//TODO: Ensure that this stays centered
			PopUpManager.centerPopUp(this.loadingIndicator);
		}
		
		/**
		 * @private
		 * Removes the loading indicator from the pop up manager. To be used
		 * when the content finishes loading, if if loading is to be cancelled.
		 */
		protected function removeLoadingIndicator():void
		{
			if(this.loadingIndicator)
			{
				PopUpManager.removePopUp(this.loadingIndicator);
				if(this.loadingIndicator is ProgressBar)
				{
					ProgressBar(this.loadingIndicator).source = null;
				}
				this.loadingIndicator = null;
			}
		}
		
		/**
		 * @private
		 * Generates the scaled-down thumbnail bitmap, if needed.
		 */
		protected function generateThumbnail():void
		{
			if(this._thumbnailSource)
			{
				return;
			}
			
			//try to use the explicit values, if possible
			var thumbnailWidth:Number = this.explicitWidth;
			var thumbnailHeight:Number = this.explicitHeight;
			if(isNaN(thumbnailWidth))
			{
				thumbnailWidth = this.explicitMaxWidth;
			}
			if(isNaN(thumbnailHeight))
			{
				thumbnailHeight = this.explicitMaxHeight;
			}
			
			if(isNaN(thumbnailWidth) && isNaN(thumbnailHeight)) 
			{
				//neither is defined
				if(this.popUpContentWidth < this.popUpContentHeight)
				{
					//if the original content height is bigger, set the height
					//to the default and calculate the width
					thumbnailHeight = DEFAULT_GENERATED_THUMBNAIL_SIZE;
					thumbnailWidth = thumbnailHeight * this.popUpContentWidth / this.popUpContentHeight;
				}
				else
				{
					thumbnailWidth = DEFAULT_GENERATED_THUMBNAIL_SIZE;
					thumbnailHeight = thumbnailWidth * this.popUpContentHeight / this.popUpContentWidth;
				}
			}
			else if(!isNaN(thumbnailWidth) || !isNaN(thumbnailHeight))
			{
				if(!isNaN(thumbnailHeight))
				{
					//height is defined, but not width
					thumbnailWidth = thumbnailHeight * this.popUpContentWidth / this.popUpContentHeight;
				}
				else if(!isNaN(thumbnailWidth))
				{
					//width is defined, but not height
					thumbnailHeight = thumbnailWidth * this.popUpContentHeight / this.popUpContentWidth;
				}
			}
			else if(this.maintainAspectRatio)
			{
				//if both are defined, and maintainAspectRatio is true,
				//then make sure they conform to the correct aspect ratio.
				//technically, we're modifying the explicit dimensions, but the
				//super class will correct for our change, so it's all good...
				if(thumbnailWidth < thumbnailHeight)
				{
					thumbnailHeight = thumbnailWidth * this.popUpContentHeight / this.popUpContentWidth;
				}
				else if(thumbnailHeight < thumbnailWidth)
				{
					thumbnailWidth = thumbnailHeight * this.popUpContentWidth / this.popUpContentHeight;
				}
			}
			
			//if we got zero for either value, we can't draw a bitmap.
			if(thumbnailWidth == 0 || thumbnailHeight == 0)
			{
				super.source = null;
				return;
			}
			
			var scaleMatrix:Matrix = new Matrix();
			scaleMatrix.scale(thumbnailWidth / this.popUpContentWidth, thumbnailHeight / this.popUpContentHeight);
			
			var bitmapCopy:BitmapData = new BitmapData(thumbnailWidth, thumbnailHeight, true, 0x00000000);
			bitmapCopy.draw(this.popUpImage, scaleMatrix, null, null, null, true);
			
			//pass it up to the old source setter to be displayed
			super.source = new BitmapAsset(bitmapCopy, PixelSnapping.AUTO, true);
		}
		
		/**
		 * @private
		 * Displays the pop-up. NOTE: The popup is expected to be on the popup
		 * manager already.
		 */
		protected function animatePopUp(delay:Boolean = false):void
		{	
			//the pop up is now being shown, so hide the loading indicator, if
			//it exists
			this.removeLoadingIndicator();
			
			var popUpBorderSize:Number = this.getStyle("popUpBorderSize");
			var popUpWidth:Number = Math.max(1, Math.min(this.popUpContentWidth, this.parentApplication.width - popUpBorderSize));
			var popUpHeight:Number = Math.max(1, Math.min(this.popUpContentHeight, this.parentApplication.height - popUpBorderSize));
			
			this.popUpImage.width = this.contentWidth;
			this.popUpImage.height = this.contentHeight;
			
			PopUpManager.centerPopUp(this.popUpImage);
			
			//make the popup grow to fit the screen
			var parallel:Parallel = new Parallel(this.popUpImage);
			parallel.duration = this.getStyle("popUpDuration");
			parallel.addEventListener(EffectEvent.EFFECT_END, growEndHandler);
			
			var resize:Resize = new Resize();
			resize.widthTo = popUpWidth;
			resize.heightTo = popUpHeight;
			parallel.addChild(resize);
			
			var move:Move = new Move();
			move.xTo = (this.parentApplication.width - popUpWidth) / 2;
			move.yTo = (this.parentApplication.height - popUpHeight) / 2;
			parallel.addChild(move);
			
			//save a reference so that it doesn't get gced
			this.popUpEffect = parallel;
			
			if(delay)
			{
				//don't play the effect until the modal background appears
				setTimeout(function():void
				{
					popUpEffect.play();
				}, this.parentApplication.getStyle("modalTransparencyDuration"));
			}
			else
			{
				this.popUpEffect.play();
			}
		}
		
		/**
		 * @private
		 * Closes (hides) the pop-up content.
		 */
		protected function hidePopUp():void
		{
			//if the pop up is hidden before it finishes loading, the loading
			//indicator may still be up there. remove it.
			this.removeLoadingIndicator();
			
			//have the popup shrink back into the source image
			var parallel:Parallel = new Parallel(this.popUpImage);
			parallel.duration = this.getStyle("popUpDuration");
			parallel.addEventListener(EffectEvent.EFFECT_END, shrinkEndHandler);
			
			var resize:Resize = new Resize();
			resize.widthTo = this.unscaledWidth;
			resize.heightTo = this.unscaledHeight;
			parallel.addChild(resize);
			
			var position:Point = new Point(0, 0);
			position = this.localToGlobal(position);
			var move:Move = new Move();
			move.xTo = position.x;
			move.yTo = position.y;
			parallel.addChild(move);
			
			parallel.play();
			this.popUpEffect = parallel;
		}
		
	//----------------------------------
	//  Protected Event Handlers
	//----------------------------------
	
		/**
		 * @private
		 * When this control is clicked, show the popup.
		 */
		protected function imageClickHandler(event:MouseEvent):void
		{	
			var popUpBorderSize:Number = this.getStyle("popUpBorderSize");
			var popUpWidth:Number = Math.max(1, Math.min(this.popUpContentWidth, this.parentApplication.width - popUpBorderSize));
			var popUpHeight:Number = Math.max(1, Math.min(this.popUpContentHeight, this.parentApplication.height - popUpBorderSize));
			
			if(!this.popUpImage || !this.popUpImage.contentHolder)
			{
				//load the popup image, if needed
				this.loadPopUpContent(true);
			}
			else
			{
				//the pop-up content has already been loaded
				//so display it right away
				this.displayPopUp();
				this.animatePopUp(true);
			}
			
			//this click event continues bubbling, so we have to stop it or the
			//system manager click handler will catch it right away!
			event.stopImmediatePropagation();
			if(this._clickToClosePopUp)
			{
				this.systemManager.addEventListener(MouseEvent.CLICK, systemManagerClickHandler, false, 0, true);
			}
		}
		
		/**
		 * @private
		 * When the popup finishes showing, listen for the click that will close it.
		 */
		protected function growEndHandler(event:EffectEvent):void
		{
			event.currentTarget.removeEventListener(EffectEvent.EFFECT_END, growEndHandler);
			this.popUpEffect = null;
			
			if(!this.clickToClosePopUp)
			{
				this.popUpImage.content.addEventListener(CloseEvent.CLOSE, popUpCloseHandler);
			}
		}
		
		/**
		 * @private
		 * When the popup is showing and the user clicks anywhere, remove the
		 * popup.
		 */
		protected function systemManagerClickHandler(event:MouseEvent):void
		{
			event.currentTarget.removeEventListener(MouseEvent.CLICK, systemManagerClickHandler);
			if(this.popUpEffect)
			{
				this.popUpEffect.stop();
				this.popUpEffect.removeEventListener(EffectEvent.EFFECT_END, growEndHandler);
				this.popUpEffect = null;
			}
			
			this.hidePopUp();
		}
		
		/**
		 * @private
		 * The the popup removal effect has finished playing, remove the popup.
		 */
		protected function shrinkEndHandler(event:EffectEvent):void
		{
			event.currentTarget.removeEventListener(EffectEvent.EFFECT_END, shrinkEndHandler);
			this.popUpEffect = null;
			PopUpManager.removePopUp(this.popUpImage);
			
			var popUpClose:PopUpThumbnailEvent = new PopUpThumbnailEvent(PopUpThumbnailEvent.POP_UP_CLOSE);
			this.dispatchEvent(popUpClose);
		}
		
		/**
		 * @private
		 * When the pop-up content finishes loading, grab the dimensions. Also,
		 * generate the thumbnail, if needed.
		 */
		protected function popUpContentLoadCompleteHandler(event:Event):void
		{
			event.currentTarget.removeEventListener(Event.COMPLETE, popUpContentLoadCompleteHandler);
			this.popUpContentWidth = this.popUpImage.contentWidth;
			this.popUpContentHeight = this.popUpImage.contentHeight;
			this.generateThumbnail();
			
			if(this.showPopUpAfterLoading)
			{
				this.animatePopUp(false);
			}
		}
		
		/**
		 * @private
		 * When the pop-up's content dispatches a close event, hide it.
		 */
		protected function popUpCloseHandler(event:CloseEvent):void
		{
			event.currentTarget.removeEventListener(CloseEvent.CLOSE, popUpCloseHandler);
			this.hidePopUp();
		}
	}
}